# Copyright 2007, 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""changebranch - stuff for change branches

Classes:
ChangeBranch -- representation of a change branch

Functions:
ListChangeBranches -- generate changebranch names and dirents
Diff            -- diff a change branch

"""

import cStringIO
import md5
import os
import posixpath
import random
import re
import shutil
import subprocess
import sys

from errno import EEXIST

import svn.diff
import svn.ra
import svn.wc

from svn.core import SVN_DIRENT_CREATED_REV, SVN_DIRENT_KIND
from svn.core import SWIG_SVN_INVALID_REVNUM as SVN_INVALID_REVNUM
from svn.core import svn_node_dir, svn_node_file, svn_node_none, svn_node_unknown
from svn.core import svn_path_local_style
from svn.core import svn_prop_regular_kind, svn_property_kind

import gvn.commit
import gvn.errors
import gvn.platform
import gvn.repository
import gvn.svncmd
import gvn.util
import gvn.wc

try:
  import simplejson
except ImportError:
  from gvn.third_party import simplejson


def IsRegularProp(name):
  (kind, prefix_len) = svn.core.svn_property_kind(name)
  return kind == svn.core.svn_prop_regular_kind

def FilterRegularProps(properties):
  """Return copy of properties with special svn properties removed."""
  return dict((name, value) for (name, value) in properties.iteritems()
              if IsRegularProp(name))


_ADD_CODES =['A', 'R']

class State(object):
  """state of a changebranch snapshot

  Attributes:
  base_path, base_revision      -- path/revision snapshot is based on
  paths                         -- dict mapping unicode path to PathState
  """
  def __init__(self, base_path, base_revision):
    self.base_path = base_path
    self.base_revision = base_revision
    self.paths = {}

  @classmethod
  def NewFromWCState(cls, base_path, wc_change_state, wc, root_url):
    """Return new object from working copy state of a snapshot.

    Arguments:
    base_path           -- path snapshot is based on
    wc_change_state     -- gvn.wc.ChangeState object
    wc                  -- gvn.wc.WorkingCopy object
    root_url            -- URL of repository root
    """
    self = cls(base_path, wc_change_state.base_revision)
    self.paths = dict((path,
                       PathState.NewFromWCState(state,
                                                wc.Entry(path),
                                                root_url))
                      for (path, state) in wc_change_state.paths.iteritems())
    return self


class PathState(object):
  """state of a path in a changebranch snapshot

  Note: kind is always svn_node_unknown for old-style changebranches,
  which are going away soon.

  Attributes:
  action                -- 'A'dd, 'D'elete, 'R'eplace, 'M'odify
  base_revision         -- revision of working copy item this represents
  snap_revision         -- revision of last snapshot
  copyfrom_path         -- path copied from or None
  copyfrom_revision     -- revision copied from or SVN_INVALID_REVNUM
  kind                  -- svn_node_kind_t
  """
  def __init__(self, action, base_revision, snap_revision,
               copyfrom_path, copyfrom_revision, kind):
    self.action = action
    self.base_revision = base_revision
    self.snap_revision = snap_revision
    self.copyfrom_path = copyfrom_path
    self.copyfrom_revision = copyfrom_revision
    self.kind = kind

  def __eq__(self, other):
    return (self.action == other.action
            and self.base_revision == other.base_revision
            and self.snap_revision == other.snap_revision
            and self.copyfrom_path == other.copyfrom_path
            and self.copyfrom_revision == other.copyfrom_revision
            and self.kind == other.kind)
  def __ne__(self, other):
    return not self == other

  @classmethod
  def NewFromStatus(cls, status, base_url):
    """Return new object from svn_wc_status2_t of a path in a snapshot.

    Arguments:
    status              -- svn_wc_status2_t
    base_url            -- URL of path snapshot is based on
    """
    if status.entry.copyfrom_url is None:
      copyfrom_path = None
      copyfrom_revision = SVN_INVALID_REVNUM
    else:
      copyfrom_path = status.entry.copyfrom_url[len(base_url) + 1:]
      copyfrom_revision = status.entry.copyfrom_rev
    return cls(gvn.wc.ActionCode(status), status.entry.revision,
               SVN_INVALID_REVNUM, copyfrom_path, copyfrom_revision,
               status.entry.kind)

  @classmethod
  def NewFromWCState(cls, wc_state, entry, base_url):
    """Return new object from working copy state of a path in a snapshot.

    Arguments:
    wc_state            -- gvn.wc.PathChangeState
    entry               -- svn_wc_entry_t
    base_url            -- URL of path snapshot is based on
    """
    if wc_state.copyfrom_url is None:
      copyfrom_path = None
      copyfrom_revision = SVN_INVALID_REVNUM
    else:
      copyfrom_path = wc_state.copyfrom_url[len(base_url) + 1:]
      copyfrom_revision = wc_state.copyfrom_revision
    return cls(wc_state.action, wc_state.revision, wc_state.snap_revision,
               copyfrom_path, copyfrom_revision, entry.kind)


def _ParseStateKind(state_dict):
  """Parse path kind from State dict (helper for ParseState)."""
  if state_dict.get('kind') in [None, 'file']:
    return svn_node_file
  return svn_node_dir

def ParseState(state_utf8, snap_revision):
  """Parse state_utf8 at snap_revision.

  Arguments:
  state_utf8            -- utf8-encoded str from the repository state file
  snap_revision         -- revision of the snapshot being examined

  Raises:
  gvn.errors.ParseState
  """
  # Decode the UTF-8 text.
  try:
    decoded = state_utf8.decode('utf8')
  except UnicodeDecodeError, e:
    raise gvn.errors.ParseState(child=gvn.errors.Encoding(e))
  # Parse the dict from the decoded text.
  try:
    parsed = simplejson.loads(decoded)
  except ValueError, e:
    raise gvn.errors.ParseState(child=e)
  # Check the format.
  try:
    format = parsed['format']
  except KeyError:
    # no format => format 0
    format = 0
  if format != 0:
    # which is all we support
    raise gvn.errors.ParseState(message='unknown format %d' % (format,))
  # Get base path and revision.
  try:
    base_path = parsed['base']
  except KeyError:
    raise gvn.errors.ParseState(message='missing base path')
  try:
    base_revision = parsed['base_rev']
  except KeyError:
    raise gvn.errors.ParseState(message='missing base revision')

  change_state = State(base_path, base_revision)
  # Get path state.
  try:
    path_state = parsed['paths']
  except KeyError:
    raise gvn.errors.ParseState(message='missing paths')
  for (path, state) in path_state.iteritems():
    try:
      action = state['action']
    except KeyError:
      raise gvn.errors.ParseState(message="missing action for '%s'" % (path,))
    # base, snap, copyfrom_path/rev, and kind may be missing.
    change_state.paths[path] = PathState(action,
                                         state.get('base', base_revision),
                                         state.get('snap', snap_revision),
                                         state.get('copyfrom_path'),
                                         state.get('copyfrom_rev',
                                                   SVN_INVALID_REVNUM),
                                         _ParseStateKind(state))
  return change_state


def _JSONEscape(s):
  return s.replace('"', r'\"')

_SERIALIZE_STATE_HEADER = '{"base": "%s", "base_rev": %d, "paths": {\n'
_SERIALIZE_STATE_FOOTER = '\n }\n}\n'

def SerializeState(change_state, suppress_snap_revision=SVN_INVALID_REVNUM):
  """Return utf8-encoded str containing JSON-serialized change_state.

  If the caller is serializing the State of a known snapshot, it specifies
  the revision of that snapshot in suppress_snap_revision; if the caller is
  serializing the state of a snapshot currently being committed, obviously
  it does not specify, so use SVN_INVALID_REVNUM.  Don't write out a "snap"
  field for paths whose snap_revision matches suppress_snap_revision.

  Arguments:
  change_state                  -- State object
  suppress_snap_revision        -- revision number of snapshot being serialized
  """
  paths = []
  for (path, path_state) in sorted(change_state.paths.iteritems(),
                                        key=lambda x: x[0]):
    items = ['     "action": "%c"' % (path_state.action,)]
    if path_state.base_revision != change_state.base_revision:
      items.append('     "base": ' + str(path_state.base_revision))

    if path_state.snap_revision != suppress_snap_revision:
      assert isinstance(path_state.snap_revision, int)
      items.append('     "snap": ' + str(path_state.snap_revision))
    if path_state.copyfrom_path is not None:
      items.append('     "copyfrom_path": "%s",\n'
                   '     "copyfrom_rev": %d'
                   % (_JSONEscape(path_state.copyfrom_path),
                      path_state.copyfrom_revision))
    if path_state.kind == svn_node_dir:
      items.append('     "kind": "dir"\n')
    paths.append('   "%s": {\n' % (_JSONEscape(path),)
                 + ',\n'.join(items)
                 + '\n   }')
  return (_SERIALIZE_STATE_HEADER % (_JSONEscape(change_state.base_path),
                                     change_state.base_revision)
          + ',\n'.join(paths)
          + _SERIALIZE_STATE_FOOTER).encode('utf8')


class _AddStateFile(gvn.commit.EditorAction):
  def __init__(self, state):
    self.state = state

  def __call__(self, parent, path, editor, pool):
    args = (path, parent,
            None,               # copyfrom_path
            SVN_INVALID_REVNUM, # copyfrom_revision
            pool)
    gvn.DiagLog('add_file%s\n', args)
    baton = editor.add_file(*args)
    args = (baton,
            None,               # base_checksum
            pool)
    gvn.DiagLog('apply_textdelta%s\n', args)
    (handler, handler_baton) = editor.apply_textdelta(*args)
    state_utf8 = SerializeState(self.state)
    svn.delta.svn_txdelta_send_string(state_utf8, handler, handler_baton, pool)
    args = (baton, md5.new(state_utf8).hexdigest(), pool)
    gvn.DiagLog('close_file%s\n', args)
    editor.close_file(*args)

class _EditStateFile(gvn.commit.EditorAction):
  def __init__(self, state, old_state, snap_revision):
    self.state = state
    self.old_state = old_state
    self.snap_revision = snap_revision

  def __call__(self, parent, path, editor, pool):
    old_state_utf8 = SerializeState(self.old_state,
                                    suppress_snap_revision=self.snap_revision)
    state_utf8 = SerializeState(self.state)

    args = (path, parent, self.snap_revision, pool)
    gvn.DiagLog('open_file%s\n', args)
    baton = editor.open_file(*args)

    args = (baton, md5.new(old_state_utf8).hexdigest(), pool)
    gvn.DiagLog('apply_textdelta%s\n', args)
    (handler, handler_baton) = editor.apply_textdelta(*args)
    svn.delta.svn_txdelta_send_string(state_utf8,
                                      handler, handler_baton, pool)
    # TODO: Actually transmit deltas.
#     source = svn.core.svn_stream_from_stringbuf(old_state_utf8)
#     # based on wc
#     target = svn.core.svn_stream_from_stringbuf(state_utf8)
#     txdelta_stream = svn.delta.svn_txdelta(source, target, pool)
#     svn.delta.svn_txdelta_send_txstream(txdelta_stream,
#                                         handler, handler_baton, pool)
#     # based on fsfs
#     target = svn.delta.svn_txdelta_target_push(handler, handler_baton,
#                                                source, pool)
#     target.write(state_utf8)
    args = (baton, md5.new(state_utf8).hexdigest(), pool)
    gvn.DiagLog('close_file%s\n', args)
    editor.close_file(*args)


class _Branch(gvn.commit.EditorAction):
  """Return a baton for copied directory.

  Raises:
  SubversionException.apr_err==SVN_ERR_FS_NOT_DIRECTORY
  SubversionException.apr_err==SVN_ERR_RA_DAV_PATH_NOT_FOUND
  SubversionException.apr_err==???
  ???

  """

  def __init__(self, copyfrom_url, copyfrom_revision,
               delete=False, delete_revision=SVN_INVALID_REVNUM):
    """Initialize.

    Arguments:
    copyfrom_url                -- internal_style str URL to copy from
    copyfrom_revision           -- int revision to copy from
    delete                     -- if True, delete target path first
                                   (default False)
    delete_revision            -- if delete, pass this revision number
                                   to delete_entry (default SVN_INVALID_REVNUM)
    """
    self.copyfrom_url = copyfrom_url
    self.copyfrom_revision = copyfrom_revision
    self.delete = delete
    self.delete_revision = delete_revision

  def __call__(self, parent, path, editor, pool):
    self.DiagLog('(%s, %d, %s, %s) => ',
                 self.copyfrom_url, self.copyfrom_revision,
                 self.delete, self.delete_revision)
    if self.delete:
      args = (path, self.delete_revision, parent, pool)
      gvn.DiagLog('delete_entry%s; ', args)
      editor.delete_entry(*args)
    args = (path, parent, self.copyfrom_url, self.copyfrom_revision, pool)
    gvn.DiagLog('add_directory%s\n', args)
    return editor.add_directory(*args)


class _WCBranch(gvn.wc.Edit):
  """Apply a change from a working copy to a change branch."""

  def __init__(self, *args, **kwargs):
    """Same as gvn.wc.Edit.__init__ plus source_revision.

    Additional arguments:
    save_state_cb   -- callback for saving new gvn.wc.PathChangeState object;
    source_revision -- revision of source branch copied to change branch
                       (required keyword argument)
    snap_revision   -- snap revision this commit is based on
    """
    self.snap_revision = kwargs.pop('snap_revision', SVN_INVALID_REVNUM)
    self.source_revision = kwargs.pop('source_revision')
    self.save_state_cb = kwargs.pop('save_state_cb')
    gvn.wc.Edit.__init__(self, *args, **kwargs)

  def _GetAction(self):
    """gvn.wc.Edit._GetAction but treat conflicted as modified."""
    try:
      action = gvn.wc.Edit._GetAction(self)
    except KeyError, e:
      if e.args[0] != svn.wc.status_conflicted:
        raise
      action = self._Modified
    return action

  def _PostProcess(self, pool):
    abs_path = self.wc.AbsolutePath(self.wc_path)
    state = gvn.wc.PathChangeState.NewFromStatus(None, abs_path,
                                                 None, self.status)
    self.save_state_cb(self.wc_path, state)

  def _Modified(self, parent, path, editor, pool):
    if (self.status.entry.revision != self.source_revision):
      self.wc.Notify(self.wc_path, svn.wc.notify_commit_modified,
                     self.status.entry.kind, pool)
      # If wc_path's base revision is not the same as the
      # source_revision the overall change branch is based on, replace
      # this node in the change branch with a copy from wc_path's base
      # and base change on that.
      if not self.status.entry.repos.startswith('http'):
        # As long as we need the 2939 hack in _AddHelper, only for non-http.
        args = (path, self.snap_revision, parent, pool)
        gvn.DiagLog('delete_entry%s; ', args)
        editor.delete_entry(*args)
      return self._AddHelper(parent, path, editor,
                             self.status.entry.url,      # copyfrom
                             self.status.entry.revision, # copyfrom
                             pool)
    else:
      return gvn.wc.Edit._Modified(self, parent, path, editor, pool,
                                   base_revision=self.snap_revision)

  # XXX workaround http://subversion.tigris.org/issues/show_bug.cgi?id=2939
  def _AddHelper(self, parent, path, editor,
                 copyfrom_url=None, copyfrom_rev=None, pool=None):
    self.DiagLog('')
    if self.status.entry.repos.startswith('http'):
      args = (path, self.snap_revision, parent, pool)
      gvn.DiagLog('delete_entry%s; ', args)
      editor.delete_entry(*args)
    result = gvn.wc.Edit._AddHelper(self, parent, path, editor,
                                    copyfrom_url, copyfrom_rev, pool)
    gvn.DiagLog('\n')
    return result


class PostfixTextDeltaManager(gvn.wc.PostfixTextDeltaManager):
  def __init__(self, wc, save_state_cb):
    gvn.wc.PostfixTextDeltaManager.__init__(self, wc)
    self.save_state_cb = save_state_cb

  def PostProcess(self, wc_path, status, checksum):
    """Update gvn change state.

    No wc bump, as this isn't a commit to the working copy's URL.
    """

    state = gvn.wc.PathChangeState.NewFromStatus(None,
                                                 self.wc.AbsolutePath(wc_path),
                                                 checksum, status)
    self.save_state_cb(wc_path, state)

  def HandleTmp(self, tmp):
    """Unlink tmp file, rather than ignoring it.

    For snapshots, we must remove this file, otherwise later submits try to
    use it as the new text base, resulting in corruption.
    """
    os.unlink(tmp)


class ChangeBranch(object):
  """Representation of a change branch."""

  def __init__(self, config, project, name, username=None,
               snap_revision=None, peg_revision=None):
    """Initialize ChangeBranch, which may or may not exist in the repo.

    A caller uses snap_revision when it knows it has the revision
    number of the snapshot it wants to work with and uses peg_revision
    if it has a revision that may or may not be the snapshot revision.
    For example, when a user offers uname/cname@REV, REV may just be a
    revision where the user knows the changebranch exists; it's not
    necessarily the snapshot revision.

    Arguments:
    config   -- gvn.config.Config
    project  -- gvn.project.Project
    name     -- name of change branch
    username -- username; defaults to config.username
    snap_revision       -- revision of snapshot, if caller knows it
    peg_revision        -- peg revision, if caller has one but no snap_revision
    """
    gvn.util.ValidateChangeName(name)

    self._config = config
    self.project = project
    self.name = name
    if username is None:
      self.username = self.project.repository.username
    else:
      self.username = username
    if snap_revision is None:
      self._revision = None
      if peg_revision is None:
        self._peg_revision = SVN_INVALID_REVNUM
      else:
        self._peg_revision = peg_revision
    else:
      self._revision = snap_revision

    self._description = None
    self._revisions = []

    self._known_to_exist = False
    self._base_path = self._base_rev = self._paths = None

    # TODO(next-release): Remove; this is just to support old-style
    # changebranches during the transition; see also _GetChangedPathsHelper
    # and BranchPath methods.
    self._branch_name = 'branch'

  def LatestName(self):
    """Return "latest" name, without snap revision.

    e.g. basil/hang-moose
    """
    return self.username + '/' + self.name

  def FullName(self):
    """Return full name of changebranch, including snap revision.

    e.g. basil/hang-moose@17
    """
    return '%s/%s@%d' % (self.username, self.name, self.revision)

  def ContainerPath(self):
    """Return root-relative path of the changebranch container.

    e.g. changes/basil/hang-moose
    """
    return self.project.ChangeBranchPath(self.username, self.name)

  def BranchPath(self, path=None):
    """Return root-relative path of (or within) the branch.

    If path is None return the branch path itself, otherwise return the
    root-relative path of path within the branch.

    e.g. BranchPath()           => changes/basil/hang-moose/branch
    e.g. BranchPath('foo')      => changes/basil/hang-moose/branch/foo
    """
    if path is None:
      path = self._branch_name
    else:
      path = self._branch_name + '/' + path
    return self.project.ChangeBranchPath(self.username, self.name, path)

  def StatePath(self):
    """Return root-relative path of the state file."""
    return self.project.ChangeBranchPath(self.username, self.name, 'state')

  def _GetRevision(self):
    if self._revision is None:
      dirent = self.project.repository.Stat(self.ContainerPath(),
                                            self._peg_revision)
      # XXX peeking
      self._revision = dirent._dirent.created_rev
      self._known_to_exist = True
    return self._revision
  revision = property(_GetRevision)

  def _GetDescription(self):
    if self._description is None:
      try:
        revision = self.revision
      except gvn.errors.RepoPath:
        # XXX I think this only fired because Create and Snapshot were
        # mixed into one; should never happen now.
        assert False
      (revision,) = self.project.repository.GetRevisions(start=revision)
      self._description = revision['svn:log'].decode('utf-8')
    return self._description
  description = property(_GetDescription)

  def _GetChangedPathsHelper(self):
    (revision,) = self.project.repository.GetRevisions(start=self.revision)

    cbp = self.project.ChangeBranchPath(self.username, self.name)

    changes = []
    branch_name = None
    for i in sorted(revision.paths, key=lambda x: x.path):
      # When we start through here, we don't know the "top name"
      # ('trunk' for '//trunk', 'projects' for '//public/projects').
      if branch_name is None:
        (dirname, basename) = posixpath.split(i.path.lstrip('/'))
        if dirname == cbp:
          # Found the branch; save branch_name, path, and copyfrom.
          self._branch_name = branch_name = basename
          cbp = self.project.ChangeBranchPath(self.username, self.name,
                                               branch_name)
          source = i.copyfrom_path
          source_revision = i.copyfrom_revision
          continue
        # If we get here, we're looking at creation of directories
        # above the changebranch; skip those.
        continue
      try:
        relative_path = gvn.util.RelativePath(cbp, i.path.lstrip('/'))
      except gvn.errors.PathNotChild:
        # i is not in our changebranch
        continue
      changes.append(gvn.repository.ChangedPath(i.path, i.revision, i.action,
                                            i.copyfrom_path, i.copyfrom_revision,
                                            relative_path, source,
                                            source_revision))
    return (source, source_revision, changes)

  def _OldGetRepoState(self):
    (source, source_revision, changes) = self._GetChangedPathsHelper()
    state = State(source.lstrip('/'), source_revision)
    for cp in changes:
      relative_path = cp.relative_path.lstrip('/')
      if cp.copyfrom_path is None:
        copyfrom_path = None
      else:
        copyfrom_path = cp.copyfrom_path.lstrip('/')
      state.paths[relative_path] = PathState(cp.action, cp.old_revision,
                                             self.revision,
                                             copyfrom_path,
                                             cp.copyfrom_revision,
                                             svn_node_unknown)
    return state

  def _NewGetRepoState(self, pool):
    """Return State object parsed from repository state file.

    Will replace GetRepoState in next release.

    Raises:
    SubversionException.apr_err in gvn.errors.SVN_NOENT
    SubversionException.apr_err==???
    gvn.errors.ParseState
    """
    sio = cStringIO.StringIO()
    svn.ra.get_file(self.project.repository.ra,
                    self.StatePath(), self.revision,
                    sio, pool)
    return ParseState(sio.getvalue(), self.revision)

  def GetRepoState(self, pool):
    """Return State object from old or new style changebranch.

    First try _NewGetRepoState, but if the state file does not exist, try
    _OldGetRepoState.

    Will be replaced by _NewGetRepoState in next release.
    """
    try:
      return self._NewGetRepoState(pool)
    except svn.core.SubversionException, e:
      if e.apr_err not in gvn.errors.SVN_NOENT:
        raise
      # No state path; try old-style changebranch.
      return self._OldGetRepoState()

  def Exists(self):
    if self._known_to_exist:
      return True
    try:
      if self._revision is None:
        # Contact the repository to find the last changed revision.
        self.revision
        return True
      # Contact the repository to check existence at revision.
      self.project.repository.Stat(self.ContainerPath(),
                                   self.revision)
      self._known_to_exist = True
      return True
    except gvn.errors.RepoPath:
      # Changebranch does not exist.
      return False

  def _MissingDirectories(self, path, revision, pool):
    """Return list of directories to make in order to mkdir path.

    In an empty repository:
      _MissingDirectories(foo/bar/baz)  => [foo, foo/bar, foo/bar/baz]
    If foo/bar already exists:
      _MissingDirectories(foo)          => []
      _MissingDirectories(foo/bar)      => []
      _MissingDirectories(foo/bar/baz)  => [foo/bar/baz]

    Arguments:
    path                -- path relative to repository root (utf8-encoded str)
    revision            -- revision the transaction is based on
    pool                -- memory pool

    Returns:
    list of utf8-encoded str
    """
    if path == '':
      return []
    kind = svn.ra.check_path(self.project.repository.ra, path, revision, pool)
    if kind != svn_node_none:
      return []
    return (self._MissingDirectories(posixpath.dirname(path), revision, pool)
            + [path])

  def Create(self, wc, path_status, pool,
             description=None):
    """Create changebranch with with keys of path_status.

    Arguments:
    wc          -- gvn.wc.WorkingCopy object open to keys of path_status
    path_status -- mapping of unicode path names relative to wc.path
                   to svn_wc_status2_t
    description -- svn:log for change; default ''
    pool        -- memory pool

    Returns:
    svn_commit_info_t for revision creating the changebranch

    Raises:
    gvn.errors.BranchFromRoot if wc.name == '' (repository root)
    """
    if wc.name == '':
      raise gvn.errors.BranchFromRoot

    if description is None:
      description = ''

    container_path_utf8 = self.ContainerPath().encode('utf8')
    branch_path_utf8 = self.BranchPath().encode('utf8')
    state_path_utf8 = self.StatePath().encode('utf8')

    head = self.project.repository.GetHead(pool)
    root_url = self.project.repository.URL()

    # Get list of paths we need to make first in order to make the
    # changebranch container.
    mkdirs = self._MissingDirectories(container_path_utf8, head, pool)
    # Initialize the repository paths for the commit editor to the
    # directories we need to make plus the branch and state paths.
    repo_paths = list(mkdirs)
    repo_paths.append(branch_path_utf8)
    repo_paths.append(state_path_utf8)

    base_path = wc.URL()[len(root_url) + 1:]
    # XXX I think this is the problem with branching from too old a
    # revision; for change/snap, this gives the revision of the
    # *shallowest* path we could come up with!
    base_revision = wc.Entry(pool=pool).revision

    # Add each modified path to the paths for the commit editor, to
    # the change state, and error out if already changebranched.
    state = State(base_path, base_revision)
    path_to_change_name = wc.MapPathToChangeName()
    for (path, status) in path_status.iteritems():
      if path in path_to_change_name:
        # ye olde stupid generic exception; we gotta fix that
        raise gvn.errors.ChangeBranch('%s already in %s'
                                      % (path, path_to_change_name[path]))
      repo_paths.append(branch_path_utf8 + '/' + path.encode('utf8'))
      state.paths[path] = PathState.NewFromStatus(status, root_url)

    wc_state = gvn.wc.ChangeState(base_revision)
    def save_state(wc_path, wc_path_state):
      wc_path_state.change_name = self.name
      wc_state.paths[wc_path] = wc_path_state
    deltaman = PostfixTextDeltaManager(wc, save_state)
    def action(path, pool):
      if path in mkdirs:
        return gvn.commit.Mkdir(head)
      elif path == state_path_utf8:
        return _AddStateFile(state)
      elif path == branch_path_utf8:
        # TODO(epg): While the new format supports property changes on
        # the wc root, we can't actually create such changebranches
        # yet (this is issue 2).  We need to check if the wc root has
        # changed; if so, do something like _WCBranch, if not, do
        # something like _Branch.
        return _Branch(wc.URL(), base_revision)
      wc_path = path[len(branch_path_utf8)+1:]
      return _WCBranch(wc, wc_path, path_status[wc_path],
                       source_revision=base_revision, save_state_cb=save_state,
                       file_baton_cb=deltaman.Add)
    try:
      try:
        commit_info = gvn.commit.Drive(self.project.repository,
                                       {'svn:log': description.encode('utf-8')},
                                       head, repo_paths, action,
                                       deltaman.Transmit)
      except svn.core.SubversionException, e:
        if e.apr_err not in [svn.core.SVN_ERR_FS_NOT_DIRECTORY,
                             svn.core.SVN_ERR_FS_NOT_FOUND]:
          # Not a case we know about; let it out.
          raise
        # Try to report decent error message for out of date parent.
        if e.apr_err == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
          # local and svn use this
          m = re.match(r"Path '(.*)' not present$", e.args[0])
          if m is None:
            # Not a case we know about; let it out.
            raise
          parent = m.group(1)[len(branch_path_utf8)+1:]
        elif e.apr_err == svn.core.SVN_ERR_FS_NOT_FOUND:
          # neon (TODO(epg): and serf?) use this
          m = re.match(r"File not found: transaction '[^']+', path '(.*)'$",
                       e.args[0])
          if m is None:
            # Not a case we know about; let it out.
            raise
          # TODO(epg): neon has leading /; does serf?
          parent = posixpath.dirname(m.group(1)[len(branch_path_utf8)+2:])
        if parent == wc.subpath:
          # Hm, our branch target itself is being flagged; obviously
          # not OutOfDateParent.
          raise
        raise gvn.errors.OutOfDateParent(e, wc.subpath, base_revision,
                                         parent)
    except:
      raise

    self._description = description
    self._revision = commit_info.revision
    self._known_to_exist = True

    # Finalize wc state.
    wc_state.Snapshot(commit_info, pool)
    wc.change_state[self.name] = wc_state
    wc.Save()
    wc.Close()

    if callable(wc.notify_func):
      wc.notify_func('Created %s.\n' % (self.FullName(),))
      if commit_info.post_commit_err is not None:
        wc.notify_func('\nWarning: %s\n' % (commit_info.post_commit_err,))

    return commit_info

  def Snapshot(self, wc, path_status, pool,
               description=None, minimal=True):
    """Snapshot changebranch with keys of path_status.

    Arguments:
    wc          -- gvn.wc.WorkingCopy object open to keys of path_status
    path_status -- mapping of unicode path names relative to wc.path
                   to svn_wc_status2_t
    description -- svn:log for change; default self.description
    minimal     -- whether to snapshot only those files which have changed
                   since last snapshot, or to snapshot everything (recreate
                   the branch); default True
    pool        -- memory pool

    Returns:
    svn_commit_info_t for revision snapshotting the changebranch
    None if minimal and nothing to snapshot
    """
    if description is None:
      description = self.description

    branch_path_utf8 = self.BranchPath().encode('utf8')
    state_path_utf8 = self.StatePath().encode('utf8')

    head = self.project.repository.GetHead(pool)
    root_url = self.project.repository.URL()

    base_path = wc.URL()[len(root_url) + 1:]
    # XXX I think this is the problem with branching from too old a
    # revision; for change/snap, this gives the revision of the
    # *shallowest* path we could come up with!
    new_base_rev = wc.Entry(pool=pool).revision

    # Assemble old repo change state.
    old_state = State.NewFromWCState(base_path, wc.change_state[self.name],
                                     wc, root_url)

    # Add each modified path to the paths for the commit editor, to
    # the change state, and error out if on another changebranch.
    repo_paths = []
    if not minimal:
      # Re-snapshotting everything, include the branch itself.
      repo_paths.append(branch_path_utf8)
    new_state = State(base_path, new_base_rev)
    new_wc_state = gvn.wc.ChangeState(new_base_rev)
    path_to_change_name = wc.MapPathToChangeName()
    iterpool = svn.core.Pool(pool)
    for (path, status) in path_status.iteritems():
      iterpool.clear()
      try:
        if path_to_change_name[path] != self.name:
          # ye olde stupid generic exception; we gotta fix that
          raise gvn.errors.ChangeBranch('%s already in %s'
                                        % (path, path_to_change_name[path]))
      except KeyError:
        # Not yet changebranched at all, so need to snap this one.
        ns = True
      else:
        ns = not minimal or wc.NeedsSnapshot(self.name, path, status, iterpool)
      if ns:
        repo_paths.append(branch_path_utf8 + '/' + path.encode('utf8'))
        # Create new repo change state.
        new_state.paths[path] = PathState.NewFromStatus(status, root_url)
        # Will create new wc change state during commit, below.
      else:
        # Copy old repo change state to new repo change state.
        new_state.paths[path] = old_state.paths[path]
        # Copy wc change state to new wc change state.
        new_wc_state.paths[path] = wc.change_state[self.name].paths[path]
    iterpool.destroy()

    if len(repo_paths) == 0:
      # In minimal snapshot mode and nothing needs snapshot; are we
      # unbranching anything?
      old_paths = set(wc.change_state[self.name].paths)
      if old_paths.difference(new_wc_state.paths):
        # Yes; so we'll modify the state file only.
        pass
      else:
        # No; we have nothing to do.
        wc.Close()
        return None

    # We're snapshotting or at least unbranching something; need to modify
    # the state file.
    repo_paths.append(state_path_utf8)

    # In minimal mode, we always need to Replace (currently, anyway;
    # if we Replace some parent of path 'foo', then we theoretically
    # don't need to Replace 'foo', but WCBranch's need-to-Replace is
    # not that smart).
    if minimal:
      wcbranch_base_rev = None
    else:
      wcbranch_base_rev = new_base_rev

    def save_state(wc_path, wc_path_state):
      wc_path_state.change_name = self.name
      new_wc_state.paths[wc_path] = wc_path_state
    deltaman = PostfixTextDeltaManager(wc, save_state)
    def action(path, pool):
      if path == state_path_utf8:
        return _EditStateFile(new_state, old_state, self.revision)
      elif path == branch_path_utf8:
        # TODO(epg): See issue 2 TODO in Create; for Snapshot, if we
        # get here at all, we need to be like _WCBranch, so no
        # additional check is needed.
        return _Branch(wc.URL(), new_base_rev, delete=True,
                       delete_revision=self.revision)
      wc_path = path[len(branch_path_utf8)+1:]
      return _WCBranch(wc, wc_path, path_status[wc_path],
                       source_revision=wcbranch_base_rev,
                       save_state_cb=save_state,
                       snap_revision=self.revision,
                       file_baton_cb=deltaman.Add)
    try:
      try:
        commit_info = gvn.commit.Drive(self.project.repository,
                                       {'svn:log': description.encode('utf-8')},
                                       head, repo_paths, action,
                                       deltaman.Transmit)
      except svn.core.SubversionException, e:
        # Try to report decent error message for out of date parent.
        if e.apr_err == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
          # local and svn use this
          m = re.match(r"Path '(.*)' not present$", e.args[0])
          if m is None:
            # Not a case we know about; let it out.
            raise
          parent = m.group(1)[len(branch_path_utf8)+1:]
          raise gvn.errors.OutOfDateParent(e, wc.subpath, new_base_rev,
                                           parent)
        elif e.apr_err == svn.core.SVN_ERR_FS_NOT_FOUND:
          # neon (TODO(epg): and serf?) use this
          m = re.match(r"File not found: transaction '[^']+', path '(.*)'$",
                       e.args[0])
          if m is None:
            # Not a case we know about; let it out.
            raise
          # TODO(epg): neon has leading /; does serf?
          parent = posixpath.dirname(m.group(1)[len(branch_path_utf8)+2:])
          raise gvn.errors.OutOfDateParent(e, wc.subpath, new_base_rev,
                                           parent)
        # Not a case we know about; let it out.
        raise
    except:
      raise

    self._description = description
    self._revision = commit_info.revision
    self._known_to_exist = True

    # Finalize wc state.
    new_wc_state.Snapshot(commit_info, pool)
    wc.change_state[self.name] = new_wc_state
    wc.Save()
    wc.Close()

    if callable(wc.notify_func):
      wc.notify_func('Snapshotted %s.\n' % (self.FullName(),))
      if commit_info.post_commit_err is not None:
        wc.notify_func('\nWarning: %s\n' % (commit_info.post_commit_err,))

    return commit_info

  def Delete(self, description=None, notify_func=None, wc=None, pool=None):
    """Delete changebranch, updating wc if not None.

    If notify_func is None but wc is not, use wc.notify_func .

    Arguments:
    description   -- svn:log for deletion; defaults to 'Delete.\\n'
    notify_func   -- optional notify callback
    wc            -- optional gvn.wc.WorkingCopy object
    pool          -- memory pool
    """
    if description is None:
      description = 'Delete.\n'
    else:
      description = description.encode('utf8')

    if notify_func is None and wc is not None:
      notify_func = wc.notify_func

    # If deleting a changebranch the wc knows nothing about (or we
    # have no wc object), getting the revision means contacting the
    # repository, which we can't do during an edit, so get it now.
    revision = self.revision

    head = self.project.repository.GetHead(pool)
    container_path_utf8 = self.ContainerPath().encode('utf8')
    def action(path, pool):
      assert path == container_path_utf8
      return gvn.commit.Delete(revision)
    commit_info = gvn.commit.Drive(self.project.repository,
                                   {'svn:log': description},
                                   head, [container_path_utf8],
                                   action, pool=pool)

    if wc is not None and self.name in wc.change_state:
      # Update local change state.
      del wc.change_state[self.name]
      wc.Save()

    if callable(notify_func):
      notify_func('Deleted %s in r%d.\n' % (self.FullName(),
                                            commit_info.revision))
      if commit_info.post_commit_err is not None:
        notify_func('\nWarning: %s\n' % (commit_info.post_commit_err,))

    return commit_info

  def Submit(self, wc, revprops=None, pool=None):
    """Submit (and remove) changebranch, updating wc.

    Arguments:
    wc          -- closed gvn.wc.WorkingCopy object
    revprops    -- optional mapping of revprop names to values
    pool        -- memory pool
    """
    if revprops is None:
      revprops = {}
    revprops['svn:log'] = self.description.encode('utf-8')
    revprops['gvn:change'] = self.FullName()

    if callable(wc.notify_func):
      wc.notify_func('Submitting %s\n' % (self.name,), pool)

    wc_paths = wc.change_state[self.name].paths
    wc.Open(wc_paths, write_lock=True)
    container_path_utf8 = self.ContainerPath().encode('utf8')
    repo_paths = [container_path_utf8]
    path_status = {}
    def status_cb(target, status):
      wc_path = wc.RelativePath(target)
      if gvn.wc.IsConflicted(status):
        raise gvn.errors.Conflict(wc_path)
      path_status[wc_path] = status
      repo_paths.append(wc.RepoPath(wc_path).encode('utf8'))
    wc.Status(wc_paths, recursive=False, get_all=False, no_ignore=False,
              show_children=False, callback=status_cb)

    head = self.project.repository.GetHead(pool)
    deltaman = gvn.wc.PostfixTextDeltaManager(wc)
    def action(path, editpool):
      if path == container_path_utf8:
        return gvn.commit.Delete(self.revision)
      wc_path = wc.LocalPath(path)
      return gvn.wc.Edit(wc, wc_path, path_status[wc_path],
                         file_baton_cb=deltaman.Add)
    try:
      commit_info = gvn.commit.Drive(self.project.repository, revprops, head,
                                     repo_paths, action,
                                     deltaman.Transmit, pool)
    except:
      raise

    # TODO(epg): Any of these may fail independently: MarkSubmitted,
    # .gvnstate updating, wc bump, and anything else we add here.  But
    # we want to try them all, anyway, but let the callers see all
    # exceptions.  Collin agreed that a composite exception class,
    # containing a list of exception objects for caller to handle, is
    # a sane way to cover this.  For now, ignore MarkSubmitted errors.
    # Eventually, the post-commit processor will add missing
    # gvn:submitted properties.
    try:
      self.MarkSubmitted(commit_info.revision, pool)
    except:
      #import traceback; traceback.print_exc()
      pass

    del wc.change_state[self.name]
    wc.Save()

    if callable(wc.notify_func):
      wc.notify_func('Submitted r%d.\n' % (commit_info.revision,), pool)
      if commit_info.post_commit_err is not None:
        wc.notify_func('\nWarning: %s\n' % (commit_info.post_commit_err,))

    wc.ProcessCommittedQueue(commit_info, pool)
    wc.Close()

    return commit_info

  def Approve(self, pool):
    """Mark this change branch approved."""

    propname = ':'.join(['gvn:approve', self.project.repository.username])
    svn.ra.change_rev_prop(self.project.repository.ra,
                           self.revision, propname, '',
                           pool)

  def MarkSubmitted(self, revision, pool):
    """Mark this changebranch submitted in revision."""
    svn.ra.change_rev_prop(self.project.repository.ra,
                           self.revision, 'gvn:submitted',
                           str(revision), pool)

def ListChangeBranches(project, dirent_fields=0, user=None, pool=None):
  """Return a generator yielding (change_name, svn_dirent_t) tuples.

  Arguments:
  project               -- gvn.project.Project
  dirent_fields         -- see svn_ra_get_dir2; use SVN_DIRENT_CREATED_REV
                           and SVN_DIRENT_KIND even if not specified here
  user                  -- user whose changebranches to yield;
                           yield all if None (the default)
  pool                  -- memory pool

  Raises:
  gvn.errors.NoChangeBranchBase
  gvn.errors.NoUserChangeBranch
  svn.core.SubversionException
  """
  if user is None:
    try:
      (dirents, revision, properties) = svn.ra.get_dir2(project.repository.ra,
                                                    project.ChangeBranchPath(),
                                                        SVN_INVALID_REVNUM,
                                                        SVN_DIRENT_KIND,
                                                        pool)
    except svn.core.SubversionException, e:
      if e.apr_err in [svn.core.SVN_ERR_FS_NOT_FOUND,
                       svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND]:
        raise gvn.errors.NoChangeBranchBase(project.ChangeBranchPath())
      raise
    users = (name for (name, dirent) in dirents.iteritems()
             if dirent.kind == svn_node_dir)
  else:
    users = [user]

  # No matter what the caller wanted, we need at least these two.
  dirent_fields |= SVN_DIRENT_CREATED_REV
  dirent_fields |= SVN_DIRENT_KIND

  iterpool = svn.core.Pool(pool)
  for user in users:
    iterpool.clear()
    try:
      (dirents, revision, properties) = svn.ra.get_dir2(
                                        project.repository.ra,
                                        project.ChangeBranchPath(user),
                                        SVN_INVALID_REVNUM,
                                        dirent_fields, pool)
    except svn.core.SubversionException, e:
      if e.apr_err in [svn.core.SVN_ERR_FS_NOT_FOUND,
                       svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND]:
        raise gvn.errors.NoUserChangeBranch(project.ChangeBranchPath(user))
      raise
    for (name, dirent) in dirents.iteritems():
      if dirent.kind == svn_node_dir:
        yield ('%s/%s@%d' % (user, name, dirent.created_rev), dirent)
  iterpool.destroy()

def UniqueChangeName(project, username, base, pool):
  """Return a unique change name starting with base.

  If base is available, just return that, otherwise try appending
  numbers to it until finding a unique name.

  """

  try:
    changebranches = [change_name for (change_name, dirent) in
                      ListChangeBranches(project, user=username, pool=pool)]
  except gvn.errors.NoUserChangeBranch:
    return base
  if base not in changebranches:
    return base

  i = 1
  while True:
    name = base + str(i)
    if name not in changebranches:
      return name
    i += 1

def RandomBranchName(length=4):
  """Return a random branchname of length 'length'.

  Branchnames are only lowercase characters and numbers, excluding:
     lower case 'o' and the number 0 so as not to be confused
     lower case 'l' and the number 1 so as not to be confused

  Letters and numbers alternate beginning with a letter.

  Examples of a 4 character branchname:  'a2i3', 'w2b8', 'd5j4'.

  Uses of names returned by this method should be wrapped in
  UniqueChangeName() to ensure uniqueness.

  """

  branchname = []
  for i in xrange(0, length):
    if i % 2 == 0:
      branchname.append(random.choice('abcdefghijkmnpqrstuvwxyz'))
    else:
      branchname.append(random.choice('23456789'))

  return ''.join(branchname)

###########################################################################
# changebranch diffing

# first, the callbacks

class DiffCallbacks(object):
  """Base class for callbacks called by Diff functions."""
  cb = None
  repo_state = None

  def SetChangeState(self, cb, state):
    """Called with ChangeBranch and State objects before any other callback.

    Callback implementors should not need to implement this one, as this
    implementation saves them as the cb and state attributes.
    """
    self.cb = cb
    self.repo_state = state

  def Directory(self, path,
                left_properties, right_properties, pool):
    """Called for added or changed directories.

    Arguments:
    path                -- unicode path relative to the base path
    left_properties     -- properties for the left side
    right_properties    -- properties for the right side
    pool                -- scratch pool
    """
    pass

  def File(self, path, left_path, right_path,
           left_properties, right_properties, pool):
    """Called for added or changed files.

    Implementations may freely modify the two tmp files, but may not
    delete them.

    Arguments:
    path                -- unicode path relative to the base path
    left_path           -- name of local temporary file for the left side
    right_path          -- name of local temporary file for the right side
    left_properties     -- properties for the left side
    right_properties    -- properties for the right side
    pool                -- scratch pool
    """
    pass

  def Abort(self, pool):
    """Called if something goes wrong before finishing the diff.

    Arguments:
    pool                -- scratch pool
    """
    pass

  def Finish(self, pool):
    """Called when the diff is finished.

    Arguments:
    pool                -- scratch pool

    Returns:
    Whatever the implementor wants (this implementation returns None); Diff
    functions will return this.
    """
    return None


class SvnDiffCallbacks(DiffCallbacks):
  """DiffCallbacks implementation using svn's internal textual diff."""

  def __init__(self, options, out_file, encoding, pool, command=None):
    """Initialize from list, file-like, str, svn.core.Pool.

    It does not make much sense for a caller to pass anything but
    sys.stdout for out_file when specify a custom command, unless it
    really wants the property diffs and 'Index' headings to go to a
    different output stream from the diff command output.

    Arguments:
    options     -- list of option strings for svn.diff.file_options_parse
    out_file    -- file-like object to which to write the diff
    encoding    -- user's encoding
    pool        -- scratch memory pool; may be cleared after instantiation
    command     -- optional custom diff; passed to subprocess.call (with shell)
    """
    self.options = svn.diff.file_options_create()
    svn.diff.file_options_parse(self.options, options, pool)
    self.fp = out_file
    self.encoding = encoding
    self.command = command
    self.result = 0

  def _DiffProps(self, path, left_properties, right_properties, pool):
    prop_diffs = svn.core.svn_prop_diffs(right_properties, left_properties,
                                         pool)
    if len(prop_diffs) == 0:
      return
    self.fp.write('\n'
                  'Property changes on: %s\n'
                  '___________________________________________________________________\n'
                  % (path.encode(self.encoding),))
    for (name, value) in prop_diffs.iteritems():
      original = left_properties.get(name)
      # TODO(glasser): Use svn.diff.mem_string stuff here.
      if original is None:
        self.fp.write('Added: %s\n'
                      '   + %s\n'
                      % (name.encode(self.encoding), value))
      elif value is None:
        self.fp.write('Deleted: %s\n'
                      '   - %s\n'
                      % (name.encode(self.encoding), original))
      else:
        self.fp.write('Modified: %s\n'
                      '   - %s\n'
                      '   + %s\n'
                      % (name.encode(self.encoding),
                         original, value))

  def Directory(self, path,
                left_properties, right_properties,
                left_header, right_header, pool):
    # TODO(epg): Support --no-diff-deleted
    no_diff_deleted = True
    if (no_diff_deleted
        and self.repo_state is not None
        and self.repo_state.paths[path].action == 'D'):
      return
    self._DiffProps(path, left_properties, right_properties, pool)

  def File(self, path, left_path, right_path,
           left_properties, right_properties,
           left_header, right_header, pool):
    # TODO(epg): Support --no-diff-deleted
    no_diff_deleted = True
    if (no_diff_deleted
        and self.repo_state is not None
        and self.repo_state.paths[path].action == 'D'):
      maybe_deleted = ' (deleted)'
    else:
      maybe_deleted = ''
    self.fp.write('Index: %s%s\n'
                  '===================================================================\n'
                  % (path.encode(self.encoding), maybe_deleted))
    if (no_diff_deleted
        and self.repo_state is not None
        and self.repo_state.paths[path].action == 'D'):
      return

    if self.command is None:
      diff = svn.diff.file_diff_2(left_path, right_path,
                                  self.options, pool)
      relative_to_dir = None
      svn.diff.file_output_unified3(self.fp, diff,
                                    left_path, right_path,
                                    left_header, right_header,
                                    self.encoding, relative_to_dir,
                                    self.options.show_c_function, pool)
      # TODO(epg): Would be nice if svn.diff could tell us if anything
      # changed so we could set self.result here.
      #self.result = ...
    else:
      cmd = '%s %s %s' % (self.command, left_path, right_path)
      gvn.DiagLog('SvnDiffCallbacks.File => subprocess.call(%s, shell=True)\n',
                  cmd)
      self.fp.flush()
      result = subprocess.call(cmd, shell=True)
      if result != 0:
        self.result = result

    self._DiffProps(path, left_properties, right_properties, pool)

  def Finish(self, pool):
    return self.result


def _MaybeWriteProperties(properties, filename):
  """Serialize properties into the file filename, if any properties.

  Helper for TkDiffCallbacks.
  """
  if len(properties) == 0:
    return
  fp = open(filename, mode='ab')
  fp.write('\n\nProperties:\n')
  for (name, value) in sorted(properties.iteritems(), key=lambda x: x[0]):
    fp.write('%s: %s\n' % (name, value))
  fp.close()

class TkDiffCallbacks(DiffCallbacks):
  """DiffCallbacks implementation using tkdiff."""

  def __init__(self, tkdiff, encoding):
    """Initialize from str, str.

    Arguments:
    tkdiff      -- tkdiff command; passed to subprocess.call (without shell)
    encoding    -- user's encoding
    """
    self.tkdiff = tkdiff
    self.encoding = encoding
    self.pairs = []

  def Directory(self, path,
                left_properties, right_properties,
                left_header, right_header, pool):
    # TODO(epg): _MaybeWriteProperties in some parallel tree (so names match
    # up) for directory prop diffs.
    return

  def File(self, path, left_path, right_path,
           left_properties, right_properties,
           left_header, right_header, pool):
    self.pairs.append((left_path, right_path))
    if len(svn.core.svn_prop_diffs(right_properties, left_properties,
                                   pool)) == 0:
      return
    _MaybeWriteProperties(left_properties, left_path)
    _MaybeWriteProperties(right_properties, right_path)

  def Finish(self, pool):
    if len(self.pairs) == 0:
      # Nothing actually changed, so don't run tkdiff.
      return 0
    argv = [self.tkdiff]
    for (left, right) in self.pairs:
      argv.append(':')
      argv.append(left)
      argv.append(right)
    return subprocess.call(argv)

#################################################################
# now, the Diff functions

# first, DiffBaseToSnapshot

def _FindLeft(path, base_revision, repo_state, copied_directory_stack):
  """Return (root-relative path, revision, header) for the left side of a diff.

  In the simple, common case this is just

    (repo_state.base_path + path, base_revision)

  But if some parent directory of path was copied
  (i.e. copied_directory_stack is not empty), we need to find the
  left side under the path from which that copy was made.

  The returned header is suitable for to svn.diff.file_output_unified3 and
  similar functions.

  Arguments:
  path                          -- base-relative path
  base_revision                 -- what the caller thinks the base revision
                                   of path is
  repo_state                    -- State object
  copied_directory_stack        -- list of copied directories
                                   (see DiffBaseToSnapshot)
  """
  if len(copied_directory_stack) > 0:
    # Some parent directory was copied; find left side in there.
    (copied_dir, copyfrom_path, copyfrom_revision) = copied_directory_stack[-1]
    path_in_copied_dir = path[len(copied_dir) + 1:]
    left_path = '/'.join([copyfrom_path, path_in_copied_dir])
    left_revision = copyfrom_revision
  else:
    # Not under copied directory; use same path for left side.
    left_path = '/'.join([repo_state.base_path, path])
    left_revision = base_revision
  # Now make the header.
  state = repo_state.paths[path]
  if (state.action == 'A'
      or (state.action == 'R' and state.copyfrom_path is not None)):
    left_header = path + '\t(added)'
  else:
    left_header = ('%s\t(^/%s@%d)'
                   % (left_path[len(repo_state.base_path) + 1:],
                      left_path, left_revision))
  return (left_path, left_revision, left_header)


def _RaGetDirOrFile(ra, path, revision, fp, dirent_fields, pool,
                    kind=svn_node_unknown):
  """Call get_file or get_dir for DiffBaseToSnapshot.

  This is for old-style changebranches, where we don't know whether path
  is a directory or file.  Arguments are a combination of svn.ra.get_file
  and svn.ra.get_dir2 arguments, plus the optional kind, which we know
  with new-style changebranches.
  """
  if kind == svn_node_file:
    result = svn.ra.get_file(ra, path, revision, fp, pool)
    return (kind, result)

  if kind == svn_node_dir:
    result = svn.ra.get_dir2(ra, path, revision, dirent_fields, pool)
    return (kind, result)

  gvn.DiagLog('_RaGetDirOrFile(%s@%d) => try get_file => ', path, revision)
  try:
    result = svn.ra.get_file(ra, path, revision, fp, pool)
    kind = svn_node_file
    gvn.DiagLog('is file')
  except svn.core.SubversionException, e:
    if e.apr_err not in [svn.core.SVN_ERR_FS_NOT_FILE,
                         # ra-neon screws us again; don't even ask
                         # about serf, which just wrote some HTML 301
                         # foo into fp!  see svn issue 3206
                         svn.core.SVN_ERR_RA_DAV_RELOCATED]:
      raise
    gvn.DiagLog('get_dir2 => ')
    result = svn.ra.get_dir2(ra, path, revision, dirent_fields, pool)
    kind = svn_node_dir
    gvn.DiagLog('is dir')
  gvn.DiagLog('\n')
  return (kind, result)


def DiffBaseToSnapshot(cb, callbacks, encoding, pool):
  """Diff the base of a snapshot to the snapshot itself.

  This is 'gvn review'; it shows the change being proposed.

  Arguments:
  cb            -- ChangeBranch object
  callbacks     -- DiffCallbacks implementation
  encoding      -- user's encoding
  pool          -- memory pool

  Returns:
  the return value of callbacks.Finish
  """
  copied_directory_stack = []
  tree = gvn.util.TmpTree()
  try:
    try:
      change_state = cb.GetRepoState(pool)
      callbacks.SetChangeState(cb, change_state)
      iterpool = svn.core.Pool(pool)
      for (path, state) in sorted(change_state.paths.iteritems(),
                                  key=lambda x: x[0]):
        iterpool.clear()
        # See if we just came out from under a copied directory.
        if len(copied_directory_stack) > 0:
          last_copied_dir = copied_directory_stack[-1][0]
          if not gvn.util.IsChild(path, last_copied_dir):
            # Yep, so pop it off the stack.
            copied_directory_stack.pop()

        kind = state.kind
        local_path = svn_path_local_style(path, iterpool).decode('utf8')
        local_path_encoded = local_path.encode(encoding)

        # Get left path/revision/header now, as we need them in a few
        # different blocks below.
        (left_path, left_revision, left_header) = _FindLeft(
          path, state.base_revision, change_state, copied_directory_stack)

        # This is more complicated than it could be in order to support
        # old-style changebranches (no repo state file), where we don't
        # know what kind of path we have.

        #################################################################
        # left side

        if kind != svn_node_dir:
          # Create tmp file, possibly incorrectly for old-style.
          fp = tree.MkFile(['left', local_path_encoded])
        else:
          fp = None

        if state.action in _ADD_CODES:
          if state.copyfrom_path is None:
            # This is an added dir or file, so no left-side properties.
            properties = {}
            if kind == svn_node_file and state.action == 'R':
              # This is a Replaced file that isn't copied; svn diffs
              # these as Modified files, so let's do the same.
              # TODO(next-release): Note that for old-style changebranches
              # we don't yet know kind, so we never get here.  That's OK,
              # this can be a teensy bit broken until we kick the old
              # style to the curb.  Don't forget to adjust testDiff in
              # test_changebranch.py
              svn.ra.get_file(cb.project.repository.ra,
                              left_path, left_revision,
                              fp, iterpool)
          else:
            # This is a copied dir or file, so get properties of copyfrom
            # for the left side.
            (kind, result) = _RaGetDirOrFile(cb.project.repository.ra,
                                             state.copyfrom_path.encode('utf8'),
                                             state.copyfrom_revision,
                                             fp, dirent_fields=0, kind=kind,
                                             pool=iterpool)
            # The properties come last in the result for both dir and file.
            properties = result[-1]
            # And push it onto the stack.
            if kind == svn_node_dir:
              copied_directory_stack.append((path, state.copyfrom_path,
                                             state.copyfrom_revision))

        else:
          # Modified or Deleted
          # TODO: Don't fetch if no_diff_deleted.
          (kind, result) = _RaGetDirOrFile(cb.project.repository.ra,
                                           left_path, left_revision,
                                           fp, dirent_fields=0, kind=kind,
                                           pool=iterpool)
          if kind == svn_node_file:
            (unused_rev, properties) = result
          else:
            (unused_dirents, unused_rev, properties) = result
        # Note whether we made a left tmp file; further down, we'll
        # delete bogus left and right files for directories in
        # old-style changebranches.
        if fp is None:
          made_left = False
        else:
          made_left = True
          fp.close()
        left_properties = FilterRegularProps(properties)

        #################################################################
        # right side

        # You might think we definitely know the kind of this path by now
        # even in the old style, but if this is an Add, we didn't do
        # anything for the left side, so we still don't know.

        branch_path = cb.BranchPath(path)
        if kind != svn_node_dir:
          # Create tmp file, possibly incorrectly for old-style.
          fp = tree.MkFile(['right', local_path_encoded])
        else:
          fp = None
        if state.action == 'D':
          properties = {}
        else:
          # Added or Modified
          branch_path_utf8 = branch_path.encode('utf8')
          (kind, result) = _RaGetDirOrFile(cb.project.repository.ra,
                                           branch_path_utf8,
                                           state.snap_revision,
                                           fp, dirent_fields=0, kind=kind,
                                           pool=iterpool)
          if kind == svn_node_file:
            (unused_rev, properties) = result
          else:
            (unused_dirents, unused_rev, properties) = result
        # Note whether we made a bogus tmp file; further down, we'll
        # delete bogus left and right files for directories in
        # old-style changebranches.
        if fp is None:
          made_right = False
        else:
          made_right = True
          fp.close()
        right_properties = FilterRegularProps(properties)

        # And here it is: if this was a directory, delete any files we
        # created incorrectly.
        if kind == svn_node_dir:
          if made_left:
            tree.RmFile(['left', local_path_encoded])
          if made_right:
            tree.RmFile(['right', local_path_encoded])

        #################################################################
        # Now diff the left and right sides.

        right_header = ('%s\t(^/%s@%d)'
                        % (path, branch_path, state.snap_revision))
        if kind == svn_node_file:
          callbacks.File(path,
                         tree.Path(['left', local_path_encoded]),
                         tree.Path(['right', local_path_encoded]),
                         left_properties, right_properties,
                         left_header, right_header,
                         iterpool)
        else:
          callbacks.Directory(path,
                              left_properties, right_properties,
                              left_header, right_header, iterpool)
      iterpool.destroy()
    except:
      callbacks.Abort(pool)
      raise
    else:
      return callbacks.Finish(pool)
  finally:
    tree.Close()


def DiffSnapshotToWC(cb, wc, callbacks, encoding, pool):
  """Diff a snapshot to the working copy.

  This is used to show what the user has changed since last snapshot.

  Arguments:
  cb            -- ChangeBranch object
  callbacks     -- DiffCallbacks implementation
  encoding      -- user's encoding
  pool          -- memory pool

  Returns:
  the return value of callbacks.Finish
  """
  paths = wc.change_state[cb.name].paths
  wc.Open(paths)
  tree = gvn.util.TmpTree()
  try:
    try:
      # Shouldn't status callback receive a pool, like most callbacks?
      # Fine, we'll handle it ourselves.
      status_pool = svn.core.Pool(pool)
      def status_cb(target, status):
        status_pool.clear()
        path = wc.RelativePath(target)
        if not wc.NeedsSnapshot(cb.name, path, status, status_pool):
          return

        branch_path = cb.BranchPath(path)
        branch_path_utf8 = branch_path.encode('utf8')
        snap_revision = paths[path].snap_revision

        left_header = ('%s\t(^/%s@%d)' % (path, branch_path, snap_revision))
        right_header = path + '\t(working copy)'

        if status.entry.kind == svn_node_dir:
          (unused_dirents, unused_rev,
           properties) = svn.ra.get_dir2(cb.project.repository.ra,
                                         branch_path_utf8,
                                         snap_revision,
                                         0, # dirent_fields
                                         status_pool)
          properties = FilterRegularProps(properties)
          (local_prop_diffs, local_props) = wc.GetPropDiffs(target, status_pool)
          gvn.util.ApplyPropDiffs(local_props, local_prop_diffs)
          callbacks.Directory(path, properties, local_props,
                              left_header, right_header, status_pool)
        else:
          local_path = svn_path_local_style(path, status_pool).decode('utf8')
          local_path_encoded = local_path.encode(encoding)

          fp = tree.MkFile(['left', local_path_encoded])
          (unused_rev, properties) = svn.ra.get_file(
            cb.project.repository.ra, branch_path_utf8,
            snap_revision, fp, status_pool)
          fp.close()
          properties = FilterRegularProps(properties)
          right_path = svn.wc.translated_file2(target.encode('utf8'),
                                               target.encode('utf8'),
                                               wc.AdmRetrieve(target,
                                                              status_pool),
                                               svn.wc.TRANSLATE_FROM_NF,
                                               status_pool)
          (local_prop_diffs, local_props) = wc.GetPropDiffs(target, status_pool)
          gvn.util.ApplyPropDiffs(local_props, local_prop_diffs)
          callbacks.File(path,
                         tree.Path(['left', local_path_encoded]), right_path,
                         properties, local_props,
                         left_header, right_header,
                         status_pool)
      wc.Status(paths.keys(),
                recursive=False, get_all=False, no_ignore=False,
                show_children=False, callback=status_cb)
      status_pool.destroy()
      wc.Close()
    except:
      callbacks.Abort(pool)
      raise
    else:
      return callbacks.Finish(pool)
  finally:
    tree.Close()


def DiffSnapshots():
  pass
  # Similar to DiffFromChange; the only way we can do this is by
  # fetching every file and comparing them.  In the common case, a
  # URL-URL diff would work, but when it doesn't, there's nothing we
  # can do in the report that will give us the behavior we need.

  # The way it would work in the common case is say:
  #    want diff between branch@20 -> branch@30
  #    report change state, telling it *what versions of branch@20 we have*
  # receive deltas telling us how to turn that into branch@30

  # That's no good:

  # r20
  # A /changes/epg/foo/branch from trunk@17
  # M /changes/epg/foo/branch/a
  # R /changes/epg/foo/branch/b from trunk@19
  # =>
  # epg/foo@20
  # M a
  # M b

  # r30
  # R /changes/epg/foo/branch from trunk@28
  # M /changes/epg/foo/branch/a
  # M /changes/epg/foo/branch/b
  # =>
  # epg/foo@30
  # M a
  # M b

  # But there are files other than a and b!  And they changed between
  # 17 and 28.  So, for example 'c': we'll receive a delta turning
  # trunk/c@17 into trunk/c@28 which is no good at all.  In large
  # trees, we could receive quite a lot of crap we don't want.
